﻿using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using S7.Net;
using S7PlcWrapper.Tools;

namespace S7PlcWrapper
{
    public class S7PlcHelper
    {
        public bool IsConnected => _plc?.IsConnected ?? false;
        public string LastErrorMsg { get; private set; }

        /// <summary>
        /// PLC连接是否可用
        /// <remarks>PLC初始化以后才有连接意义，返回ping结果</remarks>
        /// <remarks>该版本中Plc的IsAvailable属性等价于Open，因此不能在连接后使用该属性</remarks>
        /// </summary>
        public bool IsAvailable
        {
            get
            {
                if (_plc == null || string.IsNullOrWhiteSpace(_plc.IP))
                {
                    return false;
                }

                using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
                {
                    string errorMessage;
                    var ret = Connect(socket, _plc.IP, _plc.Port, out errorMessage) == ErrorCode.NoError;
                    if (!ret)
                    {
                        LastErrorMsg = errorMessage;
                    }

                    return ret;
                }
            }
        }

        /// <summary>
        /// 使用ping判断PLC是否在线
        /// <remarks>这个方法不能测试端口是否开放</remarks>
        /// </summary>
        public bool IsOnline
        {
            get
            {
                if (_plc == null || string.IsNullOrWhiteSpace(_plc.IP))
                {
                    return false;
                }

                lock (_ping)
                {
                    var reply = _ping.Send(_plc.IP, 1000);
                    return reply != null && reply.Status == IPStatus.Success;
                }
            }
        }

        private Plc _plc;
        private readonly Ping _ping;

        public S7PlcHelper()
        {
            _ping = new Ping();
        }

        public void Init(CpuType cpu, string ip, int port, short rack, short slot)
        {
            _plc = new Plc(cpu, ip, port, rack, slot);
        }

        public void Init(CpuType cpu, string ip, short rack, short slot)
        {
            _plc = new Plc(cpu, ip, rack, slot);
        }

        public void UnInit()
        {
            (_plc as IDisposable)?.Dispose();
            _plc = null;
        }

        public async Task OpenAsync(CancellationToken cancellationToken)
        {
            if (_plc != null)
            {
                try
                {
                    await _plc.OpenAsync(cancellationToken);
                }
                catch (Exception e)
                {
                    LogHelper.AddErrorLog(e.Message);
                }
            }
        }

        public void Close()
        {
            _plc?.Close();
        }

        #region ReadVariable

        public bool ReadBitVariable(string variable, out bool val)
        {
            try
            {
                val = (bool) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = false;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        public bool ReadByteVariable(string variable, out byte val)
        {
            try
            {
                val = (byte) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = 0;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        public bool ReadWordVariable(string variable, out ushort val)
        {
            try
            {
                val = (ushort) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = 0;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        public bool ReadDWordVariable(string variable, out uint val)
        {
            try
            {
                val = (ushort) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = 0;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        public bool ReadFloatVariable(string variable, out float val)
        {
            try
            {
                val = (float) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = 0;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        public bool ReadDoubleVariable(string variable, out double val)
        {
            try
            {
                val = (double) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = 0;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        public bool ReadDWord2Variable(string variable, out uint val)
        {
            try
            {
                val = (ushort) _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                val = 0;
                LogHelper.AddErrorLog(e.Message);
                return false;
            }
        }

        #endregion

        #region General Read/Write

        public bool ReadVariable(string variable, out object val)
        {
            val = null;

            if (!_plc.IsConnected)
            {
                return false;
            }

            try
            {
                val = _plc.Read(variable);
                return true;
            }
            catch (Exception e)
            {
                LastErrorMsg = e.Message;
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }
        }

        public bool WriteVariable(string variable, object val)
        {
            if (!_plc.IsConnected)
            {
                return false;
            }

            try
            {
                _plc.Write(variable, val);
                return true;
            }
            catch (Exception e)
            {
                LastErrorMsg = e.Message;
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }
        }

        public bool ReadString(string variable, out string val)
        {
            val = string.Empty;

            if (!_plc.IsConnected)
            {
                return false;
            }

            DataType dataType;
            int dbNumber;
            int address;
            try
            {
                VarType varType;
                int bitNumber;
                S7PlcProtocol.Parse(variable, out dataType, out dbNumber, out varType, out address, out bitNumber);
            }
            catch (Exception e)
            {
                LastErrorMsg = $@"变量名称[{variable}]解析失败：{e.Message}";
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }

            try
            {
                var count = (byte) _plc.Read(dataType, dbNumber, address, VarType.Byte, 1);
                val = (string) _plc.Read(dataType, dbNumber, address + 1, VarType.String, count);
                return true;
            }
            catch (Exception e)
            {
                LastErrorMsg = e.Message;
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }
        }

        public bool WriteString(string variable, string val)
        {
            if (!_plc.IsConnected)
            {
                return false;
            }

            DataType dataType;
            int dbNumber;
            int address;
            try
            {
                VarType varType;
                int bitNumber;
                S7PlcProtocol.Parse(variable, out dataType, out dbNumber, out varType, out address, out bitNumber);
            }
            catch (Exception e)
            {
                LastErrorMsg = $@"变量名称[{variable}]解析失败：{e.Message}";
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }

            try
            {
                _plc.Write(dataType, dbNumber, address, (byte) val.Length);
                _plc.Write(dataType, dbNumber, address + 1, val);
                return true;
            }
            catch (Exception e)
            {
                LastErrorMsg = e.Message;
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }
        }

        public bool ReadS7String(string variable, out string val)
        {
            val = string.Empty;

            if (!_plc.IsConnected)
            {
                return false;
            }

            DataType dataType;
            int dbNumber;
            int address;
            try
            {
                VarType varType;
                int bitNumber;
                S7PlcProtocol.Parse(variable, out dataType, out dbNumber, out varType, out address, out bitNumber);
            }
            catch (Exception e)
            {
                LastErrorMsg = $@"变量名称[{variable}]解析失败：{e.Message}";
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }

            try
            {
                var reservedLength = (byte) _plc.Read(dataType, dbNumber, address, VarType.Byte, 1);
                val = (string) _plc.Read(dataType, dbNumber, address, VarType.S7String, reservedLength);
                return true;
            }
            catch (Exception e)
            {
                LastErrorMsg = e.Message;
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }
        }

        public bool WriteS7String(string variable, string val)
        {
            if (!_plc.IsConnected)
            {
                return false;
            }

            DataType dataType;
            int dbNumber;
            int address;
            try
            {
                VarType varType;
                int bitNumber;
                S7PlcProtocol.Parse(variable, out dataType, out dbNumber, out varType, out address, out bitNumber);
            }
            catch (Exception e)
            {
                LastErrorMsg = $@"变量名称[{variable}]解析失败：{e.Message}";
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }

            try
            {
                var temp = Encoding.ASCII.GetBytes(val);
                var bytes = S7.Net.Types.S7String.ToByteArray(val, temp.Length);
                _plc.WriteBytes(dataType, dbNumber, address, bytes);
                return true;
            }
            catch (Exception e)
            {
                LastErrorMsg = e.Message;
                LogHelper.AddErrorLog(LastErrorMsg);
                return false;
            }
        }

        #endregion

        /// <summary>
        /// 连接Socket
        /// </summary>
        /// <param name="socket">socket对象</param>
        /// <param name="ip">IP地址</param>
        /// <param name="port">端口</param>
        /// <param name="errorMessage">错误信息</param>
        /// <returns>连接状态码</returns>
        private static ErrorCode Connect(Socket socket, string ip, int port, out string errorMessage)
        {
            var errorCode = ErrorCode.NoError;
            errorMessage = string.Empty;

            try
            {
                var server = new IPEndPoint(IPAddress.Parse(ip), port);
                socket.Connect(server);
                return errorCode;
            }
            catch (SocketException sex)
            {
                // see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668(v=vs.85).aspx
                errorCode = sex.SocketErrorCode == SocketError.TimedOut
                    ? ErrorCode.IPAddressNotAvailable
                    : ErrorCode.ConnectionError;
                errorMessage = sex.Message;
            }
            catch (Exception ex)
            {
                errorCode = ErrorCode.ConnectionError;
                errorMessage = ex.Message;
            }

            return errorCode;
        }

        /// <summary>
        /// 对PLC进行ping操作
        /// </summary>
        /// <param name="ip">IP地址</param>
        /// <returns>是否可以ping通</returns>
        private static bool PingPlc(string ip)
        {
            var ping = new Ping();
            var reply = ping.Send(ip, 1000);
            return reply != null && reply.Status == IPStatus.Success;
        }
    }
}
